文章摘要 FakeGPT
加载中...|
概述
大语言模型虽然能力强大,但存在两个核心问题:
- 知识截止:训练数据有时间限制,不知道最新信息
- 幻觉问题:可能编造不存在的事实
RAG(Retrieval-Augmented Generation,检索增强生成)通过让 LLM 检索外部知识库来回答问题,有效解决了这些问题。本文将深入解析 RAG 技术的原理和实践。
什么是 RAG
RAG 的定义
RAG 是一种结合了信息检索和生成式 AI的技术,它的工作流程是:
text
┌─────────────────────────────────────────────────────────┐
│ RAG 工作流程 │
├─────────────────────────────────────────────────────────┤
│ │
│ 用户问题 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 检索相关文档 (Retriever) │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 将检索结果作为上下文 (Context) │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ LLM 基于上下文生成回答 (Generator) │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 最终回答 (带来源引用) │
│ │
└─────────────────────────────────────────────────────────┘RAG 的优势
| 优势 | 说明 |
|---|---|
| 实时信息 | 可以获取最新知识 |
| 可解释性 | 回答可以引用来源 |
| 私有数据 | 可以利用企业私有数据 |
| 减少幻觉 | 基于真实文档回答 |
| 成本低 | 不需要微调模型 |
RAG vs Fine-tuning
text
┌─────────────────────────────────────────────────────────┐
│ RAG vs Fine-tuning │
├─────────────────────────────────────────────────────────┤
│ │
│ RAG 的优势: │
│ • 实时更新知识 │
│ • 可以引用来源 │
│ • 实现简单成本低 │
│ • 适合知识密集型任务 │
│ │
│ Fine-tuning 的优势: │
│ • 学习特定风格/格式 │
│ • 减少延迟(不需要检索) │
│ • 适合行为调整 │
│ │
│ 组合使用: │
│ RAG + Fine-tuning = 最佳效果 │
│ │
└─────────────────────────────────────────────────────────┘选择建议
| 场景 | 推荐方案 |
|---|---|
| 需要最新信息 | RAG |
| 需要引用来源 | RAG |
| 私有数据问答 | RAG |
| 特定输出格式 | Fine-tuning |
| 特定说话风格 | Fine-tuning |
| 复杂推理任务 | RAG + Fine-tuning |
RAG 的核心流程
完整流程图
text
┌─────────────────────────────────────────────────────────┐
│ RAG 完整流程 │
├─────────────────────────────────────────────────────────┤
│ │
│ 【准备阶段 - 离线】 │
│ │
│ 文档 → 加载 → 分块 → Embedding → 向量数据库 │
│ │
│ 【查询阶段 - 在线】 │
│ │
│ 用户问题 → Embedding → 向量检索 → Top-K 文档 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Prompt: "基于以下文档回答问题..." │ │
│ │ │ │
│ │ [文档1] │ │
│ │ [文档2] │ │
│ │ [文档3] + 问题 │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ LLM 生成回答 │
│ │
└─────────────────────────────────────────────────────────┘步骤详解
1. 文档加载 (Document Loading)
python
from langchain.document_loaders import (
TextLoader,
PyPDFLoader,
DirectoryLoader,
WebBaseLoader
)
# 加载单个文本文件
loader = TextLoader("doc.txt")
documents = loader.load()
# 加载 PDF
pdf_loader = PyPDFLoader("report.pdf")
pdf_docs = pdf_loader.load()
# 加载整个目录
dir_loader = DirectoryLoader(
"./docs",
glob="**/*.md",
loader_cls=TextLoader
)
all_docs = dir_loader.load()
# 加载网页
web_loader = WebBaseLoader([
"https://example.com/doc1",
"https://example.com/doc2"
])
web_docs = web_loader.load()2. 文档分块 (Chunking)
python
from langchain.text_splitter import (
RecursiveCharacterTextSplitter,
TokenTextSplitter
)
# 字符分块
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 每块大小
chunk_overlap=200, # 重叠大小
length_function=len,
separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""]
)
chunks = text_splitter.split_documents(documents)
# Token 分块
token_splitter = TokenTextSplitter(
chunk_size=500, # token 数量
chunk_overlap=50,
encoding_name="cl100k_base"
)
token_chunks = token_splitter.split_documents(documents)3. 向量化 (Embedding)
python
from langchain_openai import OpenAIEmbeddings
from langchain_community.embeddings import HuggingFaceEmbeddings
# OpenAI Embeddings
openai_embeddings = OpenAIEmbeddings(
model="text-embedding-3-small"
)
# 本地 Embeddings
local_embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
# 生成向量
texts = [chunk.page_content for chunk in chunks]
vectors = openai_embeddings.embed_documents(texts)4. 向量存储 (Vector Store)
python
from langchain.vectorstores import Chroma, Pinecone, FAISS
from langchain_community.vectorstores import Qdrant
# Chroma (本地,最简单)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=openai_embeddings,
persist_directory="./chroma_db"
)
# FAISS (本地,高效)
faiss_store = FAISS.from_documents(
documents=chunks,
embedding=openai_embeddings
)
faiss_store.save_local("faiss_index")
# Pinecone (云端)
pinecone_store = Pinecone.from_documents(
documents=chunks,
embedding=openai_embeddings,
index_name="my-rag-index"
)
# Qdrant (本地/云端)
qdrant_store = Qdrant.from_documents(
documents=chunks,
embedding=openai_embeddings,
url="http://localhost:6333",
collection_name="my_collection"
)5. 检索 (Retrieval)
python
# 相似度检索
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 4} # 返回 Top-4
)
# 相似度得分检索
retriever = vectorstore.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={
"k": 4,
"score_threshold": 0.7 # 相似度阈值
}
)
# MMR 检索(多样性)
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 4, "lambda_mult": 0.5}
)
# 执行检索
results = retriever.get_relevant_documents("什么是 RAG?")
for i, doc in enumerate(results):
print(f"文档 {i+1}:")
print(f"内容: {doc.page_content[:100]}...")
print(f"元数据: {doc.metadata}")
print()6. 生成 (Generation)
python
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
# 自定义 Prompt
prompt_template = """请基于以下文档回答问题。如果文档中没有相关信息,请说"我不知道"。
文档内容:
{context}
问题: {question}
回答:"""
PROMPT = PromptTemplate(
template=prompt_template,
input_variables=["context", "question"]
)
# 创建 RAG 链
llm = ChatOpenAI(model="gpt-4o", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
chain_type_kwargs={"prompt": PROMPT}
)
# 查询
result = qa_chain.invoke({"query": "什么是 RAG?"})
print("回答:", result['result'])
print("\n来源文档:")
for doc in result['source_documents']:
print(f"- {doc.metadata.get('source', 'unknown')}")文档分块策略
分块策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 固定大小 | 简单高效 | 可能切断语义 | 通用文档 |
| 语义分块 | 保持语义完整 | 计算成本高 | 复杂文档 |
| 递归分块 | 平衡效果和性能 | 需要调参 | 大多数场景 |
| 父文档检索 | 保留上下文 | 存储开销大 | 长文档 |
递归字符分块(推荐)
python
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 目标块大小
chunk_overlap=200, # 重叠大小(保持上下文)
length_function=len,
separators=[
"\n\n", # 段落
"\n", # 行
"。", # 中文句号
"!", # 中文感叹号
"?", # 中文问号
",", # 中文逗号
" ", # 空格
"" # 字符(最后手段)
]
)
chunks = text_splitter.split_documents(documents)父文档检索
python
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
# 子文档(用于检索)
child_splitter = RecursiveCharacterTextSplitter(
chunk_size=400
)
# 父文档(用于生成)
parent_splitter = RecursiveCharacterTextSplitter(
chunk_size=2000
)
# 创建检索器
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=InMemoryStore(),
child_splitter=child_splitter,
parent_splitter=parent_splitter
)
# 添加文档
retriever.add_documents(documents)
# 检索时返回父文档
results = retriever.get_relevant_documents("问题")分块大小建议
| 文档类型 | Chunk Size | Overlap |
|---|---|---|
| 短文本 | 256-512 | 50 |
| 一般文章 | 512-1024 | 100-200 |
| 长文档 | 1024-2048 | 200-400 |
| 代码 | 500-1000 | 50-100 |
检索策略
1. 相似度检索
python
# 基础相似度检索
retriever = vectorstore.as_retriever(
search_kwargs={"k": 5}
)
# 带阈值
retriever = vectorstore.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={
"k": 5,
"score_threshold": 0.7
}
)2. MMR 检索(多样性)
text
┌─────────────────────────────────────────────────────────┐
│ MMR 原理 │
├─────────────────────────────────────────────────────────┤
│ │
│ 目标:平衡相关性和多样性 │
│ │
│ lambda → 1: 只看相关性(可能重复) │
│ lambda → 0: 只看多样性(可能不相关) │
│ lambda = 0.5: 平衡(推荐) │
│ │
└─────────────────────────────────────────────────────────┘python
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={
"k": 5,
"lambda_mult": 0.5
}
)3. 混合检索
python
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
# 向量检索
vector_retriever = vectorstore.as_retriever(
search_kwargs={"k": 5}
)
# 关键词检索 (BM25)
bm25_retriever = BM25Retriever.from_documents(
documents=chunks
)
bm25_retriever.k = 5
# 混合检索
ensemble_retriever = EnsembleRetriever(
retrievers=[vector_retriever, bm25_retriever],
weights=[0.5, 0.5] # 权重
)4. 重排序 (Reranking)
python
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
# 加载重排序模型
reranker = HuggingFaceCrossEncoder(
model_name="BAAI/bge-reranker-base"
)
# 检索更多文档
docs = retriever.get_relevant_documents("问题", k=20)
# 重排序
reranked_docs = reranker.compress_documents(
documents=docs,
query="问题"
)
# 取 Top-K
final_docs = reranked_docs[:5]RAG 优化技巧
1. 查询扩展
python
def query_expansion(query: str, llm) -> list:
"""扩展查询为多个变体"""
prompt = f"""生成 3 个不同方式表达的查询,意图与以下查询相同:
查询: {query}
只输出查询,每行一个。"""
result = llm.invoke(prompt)
queries = [query] + result.content.split('\n')
return [q.strip() for q in queries if q.strip()]
# 使用扩展查询
queries = query_expansion("如何提高 RAG 效果?", llm)
all_docs = []
for q in queries:
docs = retriever.get_relevant_documents(q, k=2)
all_docs.extend(docs)
# 去重
unique_docs = list({d.page_content: d for d in all_docs}.values())2. 元数据过滤
python
# 自定义检索器
def metadata_filter_retrieval(query: str, filters: dict):
"""结合元数据过滤的检索"""
# 先按相似度检索
docs = vectorstore.similarity_search(query, k=20)
# 再按元数据过滤
filtered_docs = []
for doc in docs:
match = True
for key, value in filters.items():
if doc.metadata.get(key) != value:
match = False
break
if match:
filtered_docs.append(doc)
return filtered_docs[:5]
# 使用
results = metadata_filter_retrieval(
"API 使用方法",
filters={"category": "技术文档", "year": 2024}
)3. 上下文压缩
python
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=retriever
)
# 只返回与问题相关的内容
compressed_docs = compression_retriever.get_relevant_documents("问题")常见问题与解决方案
问题 1:检索不相关
原因:
- 分块不合理
- Embedding 模型不匹配
- 查询表达不清
解决方案:
python
# 1. 优化分块
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 更小的块
chunk_overlap=100,
separators=["\n\n", "\n", "。", "!", "?"]
)
# 2. 使用领域 Embeddings
from langchain_community.embeddings import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-large-zh-v1.5" # 中文优化
)
# 3. 查询重写
expanded_queries = query_expansion(original_query, llm)问题 2:回答不完整
原因:
- 检索的文档不够
- 上下文窗口限制
解决方案:
python
# 1. 增加检索数量
retriever = vectorstore.as_retriever(
search_kwargs={"k": 10} # 增加 k 值
)
# 2. 使用父文档检索
parent_retriever = ParentDocumentRetriever(...)
# 3. 迭代检索
def iterative_retrieval(query, max_iterations=3):
context = ""
for i in range(max_iterations):
docs = retriever.get_relevant_documents(query)
new_info = "\n".join([d.page_content for d in docs])
if new_info not in context:
context += "\n" + new_info
# 检查是否足够
if len(context) > 3000:
break
# 生成子问题
query = llm.invoke(f"基于: {context}\n生成下一个子问题")
return context问题 3:回答仍有幻觉
原因:
- 检索文档质量差
- Prompt 没有约束
解决方案:
python
# 1. 改进 Prompt
strict_prompt = """请严格基于以下文档回答问题。
如果文档中没有答案,请说"根据提供的文档,我无法回答这个问题"。
文档:
{context}
问题: {question}
回答:"""
# 2. 添加来源验证
def add_citations(response, source_docs):
"""在回答中添加引用"""
citations = "\n\n来源:\n"
for doc in source_docs:
citations += f"- {doc.metadata.get('source', 'unknown')}\n"
return response + citations
# 3. 使用 Reranker 过滤低质量文档
reranker = HuggingFaceCrossEncoder(...)RAG 系统评估
评估指标
| 指标 | 说明 | 计算方法 |
|---|---|---|
| Precision | 检索准确率 | 相关文档数 / 检索文档数 |
| Recall | 检索召回率 | 检索到相关文档数 / 总相关文档数 |
| F1 | 综合指标 | 2 × P × R / (P + R) |
| MRR | 平均倒数排名 | 1 / 第一个相关文档排名 |
| NDCG | 排序质量 | 考虑位置的评分 |
评估代码示例
python
from ragas import evaluate
from datasets import Dataset
# 准备评估数据
test_data = {
"question": ["什么是 RAG?", "如何优化 RAG?"],
"answer": ["RAG 是...", "优化方法包括..."],
"contexts": [
["文档1内容", "文档2内容"],
["文档3内容", "文档4内容"]
],
"ground_truth": ["RAG 的正确答案", "优化方法的正确答案"]
}
dataset = Dataset.from_dict(test_data)
# 评估
result = evaluate(
dataset=dataset,
metrics=[
"faithfulness", # 忠实度
"answer_relevancy", # 相关性
"context_precision",# 上下文精确度
"context_recall" # 上下文召回率
]
)
print(result)小结
RAG 是让 LLM 能够利用外部知识库的关键技术:
核心要点
RAG 流程
- 文档加载 → 分块 → 向量化 → 存储
- 查询向量化 → 检索 → 生成回答
关键技巧
- 合理的分块策略(递归分块推荐)
- 混合检索(向量 + 关键词)
- 重排序提高精度
- 查询扩展和重写
优化方向
- 检索质量:更好的分块、Embedding、重排序
- 生成质量:优化的 Prompt、上下文压缩
- 系统性能:缓存、批量处理、更快的模型
与微调的关系
- RAG 和 Fine-tuning 是互补的
- 可以组合使用获得最佳效果
下一篇文章将详细介绍向量数据库,它是 RAG 系统的核心组件。
赞赏博主
评论 隐私政策